<!-- 代码已包含 CSS：使用 TailwindCSS , 安装 TailwindCSS 后方可看到布局样式效果 -->
<template>
    <div class="min-h-screen bg-gray-50 overflow-x-hidden">
        <!-- 顶部导航 -->
        <nav class="bg-gradient-to-r from-indigo-600 to-purple-600">
            <div class="max-w-7xl w-full mx-auto relative overflow-hidden px-4">
                <div class="absolute inset-0 opacity-10">
                    <div class="absolute -inset-[100%] bg-[radial-gradient(circle_at_50%_120%,rgba(255,255,255,0.2),transparent_80%)]"></div>
                </div>
                <div class="flex items-center justify-center gap-6 py-5 px-8 relative">
                    <div
                        class="bg-white/10 p-3 rounded-2xl backdrop-blur-sm shadow-lg shadow-white/5 border border-white/10 group hover:bg-white/20 transition-all duration-300"
                    >
                        <i class="fas fa-microphone text-white text-2xl group-hover:scale-110 transition-transform"></i>
                    </div>
                    <div class="flex flex-col items-center">
                        <h1 class="text-2xl font-bold text-white tracking-wider">JBoltAI智能语音笔记</h1>
                        <div class="text-indigo-200 text-sm mt-1">AI 驱动的智能语音笔记转写工具</div>
                    </div>
                </div>
            </div>
        </nav>
        <main class="w-full max-w-full mx-auto px-4 py-12">
            <!-- 录音控制区 -->
            <div class="flex flex-col items-center mb-6">
                <div class="relative mb-4">
                    <div
                        :class="{ 'pulse-animation': isRecording }"
                        class="w-24 h-24 rounded-full bg-gradient-to-r from-indigo-500 to-purple-500 flex items-center justify-center cursor-pointer shadow-xl hover:shadow-2xl transition-all duration-300 transform hover:scale-105"
                        @click="toggleRecording"
                    >
                        <i :class="[isRecording ? 'fa-stop' : 'fa-microphone', 'fas text-white text-3xl']"></i>
                    </div>
                    <div
                        v-if="isRecording"
                        class="absolute -top-2 -left-2 -right-2 -bottom-2 rounded-full border-4 border-indigo-300 animate-ping pointer-events-none"
                    ></div>
                </div>
                <div class="text-xl font-medium mb-3">
                    {{ formatTime }} {{ isRecording ? "(录音中，点击暂停)" : recordingTime > 0 ? "(已暂停，点击继续录音)" : "" }}
                </div>
                <div class="flex flex-col sm:flex-row gap-3">
                    <button
                        class="!rounded-button px-8 py-3 bg-white border border-gray-200 text-gray-700 hover:bg-gray-50 flex items-center gap-3 whitespace-nowrap shadow-sm transition-all duration-300 hover:border-gray-300 hover:-translate-y-0.5 active:translate-y-0 hover:shadow-lg active:shadow-md"
                        @click="resetRecording"
                    >
                        <i class="fas fa-redo transition-transform hover:rotate-180"></i>
                        重新录制
                    </button>
                    <button
                        class="!rounded-button px-8 py-3 bg-gradient-to-r from-indigo-600 to-purple-600 text-white hover:from-indigo-700 hover:to-purple-700 flex items-center gap-3 whitespace-nowrap shadow-md transition-all duration-300 hover:shadow-lg hover:-translate-y-0.5 active:translate-y-0 active:shadow-md min-w-[144px] justify-center"
                        @click="completeTranscription"
                        :disabled="isTranscribing"
                    >
                        <template v-if="isTranscribing">
                            <div class="typing-indicator scale-50">
                                <span></span>
                                <span></span>
                                <span></span>
                            </div>
                            转写中...
                        </template>
                        <template v-else>
                            <i class="fas fa-check transition-transform hover:scale-110"></i>
                            完成转写
                        </template>
                    </button>
                </div>
            </div>
            <!-- 转写内容展示区 -->
            <div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-10 w-full px-4">
                <!-- 实时转写内容 -->
                <div class="bg-gradient-to-br from-white to-indigo-50/30 backdrop-blur-sm rounded-3xl shadow-xl p-8 border border-indigo-100/50">
                    <div class="flex justify-between items-center mb-6">
                        <div class="flex items-center gap-3">
                            <div class="w-2 h-8 bg-gradient-to-b from-indigo-600 to-purple-600 rounded-full"></div>
                            <h2 class="text-xl font-semibold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
                                实时转写内容
                            </h2>
                        </div>
                        <div class="flex gap-4">
                            <button class="hover:bg-indigo-50 p-2 rounded-xl transition-all duration-300 group" @click="copyRealTimeText">
                                <i class="fas fa-copy text-lg text-gray-400 group-hover:text-indigo-600 transition-colors"></i>
                            </button>
                        </div>
                    </div>
                    <div
                        class="h-[300px] md:h-[400px] rounded-2xl p-4 md:p-8 relative bg-gradient-to-br from-white to-indigo-50/50 border border-indigo-100/30 shadow-inner"
                        :class="{ 'custom-scrollbar': realtimeText }"
                        ref="realtimeContainer"
                    >
                        <div
                            class="w-full h-full bg-transparent text-gray-600 leading-relaxed text-lg font-sans whitespace-pre-wrap"
                        >{{ realtimeText }}</div>
                        <div v-if="!realtimeText" class="absolute inset-0 flex flex-col items-center justify-center gap-4 py-12 pointer-events-none">
                            <div class="w-16 h-16 rounded-full bg-indigo-50 flex items-center justify-center">
                                <i class="fas fa-microphone text-2xl text-indigo-300"></i>
                            </div>
                            <p class="text-gray-400 text-lg">点击上方录音按钮开始录制</p>
                        </div>
                        <div v-if="isRecording" class="absolute bottom-6 left-1/2 -translate-x-1/2">
                            <div class="typing-indicator scale-150">
                                <span></span>
                                <span></span>
                                <span></span>
                            </div>
                        </div>
                    </div>
                </div>
                <!-- AI 转写内容 -->
                <div class="bg-gradient-to-br from-white to-indigo-50/30 backdrop-blur-sm rounded-3xl shadow-xl p-8 border border-indigo-100/50">
                    <div class="flex justify-between items-center mb-6">
                        <div class="flex items-center gap-3">
                            <div class="w-2 h-8 bg-gradient-to-b from-indigo-600 to-purple-600 rounded-full"></div>
                            <h2 class="text-xl font-semibold bg-gradient-to-r from-indigo-600 to-purple-600 bg-clip-text text-transparent">
                                AI 优化
                            </h2>
                        </div>
                        <div class="flex gap-4">
                            <button class="hover:bg-indigo-50 p-2 rounded-xl transition-all duration-300 group" @click="copyText">
                                <i class="fas fa-copy text-lg text-gray-400 group-hover:text-indigo-600 transition-colors"></i>
                            </button>
                            <button class="hover:bg-indigo-50 p-2 rounded-xl transition-all duration-300 group" @click="exportText">
                                <i class="fas fa-download text-lg text-gray-400 group-hover:text-indigo-600 transition-colors"></i>
                            </button>
                        </div>
                    </div>
                    <div
                        class="h-[300px] md:h-[400px] overflow-y-auto rounded-2xl p-4 md:p-8 relative bg-gradient-to-br from-white to-indigo-50/50 border border-indigo-100/30 shadow-inner custom-scrollbar"
                        ref="transcriptContainer"
                    >
                        <!-- 遮罩层 -->
                        <div v-if="isTranscribing" class="absolute inset-0 backdrop-blur-sm bg-white/70 flex items-center justify-center z-10">
                            <div class="flex flex-col items-center gap-4">
                                <div class="typing-indicator scale-150">
                                    <span></span>
                                    <span></span>
                                    <span></span>
                                </div>
                                <p class="text-indigo-600 font-medium text-lg tracking-wide animate-pulse">
                                    正在优化中...
                                </p>
                            </div>
                        </div>
                        <p v-if="transcriptText" class="whitespace-pre-line text-gray-600 leading-relaxed text-lg">{{ transcriptText }}</p>
                        <div v-else-if="!isTranscribing" class="flex flex-col items-center justify-center h-full gap-4 py-12">
                            <div class="w-16 h-16 rounded-full bg-indigo-50 flex items-center justify-center">
                                <i class="fas fa-wand-magic-sparkles text-2xl text-indigo-300"></i>
                            </div>
                            <p class="text-gray-400 text-lg">点击上方完成转写按钮开始优化...</p>
                        </div>
                    </div>
                </div>
            </div>
        </main>
    </div>
</template>
<script lang="ts" setup>
import { ref, computed, createApp, h, onMounted, watch } from "vue";
import { JBoltApi } from "@/service/request/index.js";
import { useWebSocketStore } from "@/stores/websocket.js";
import AiChat from "@/components/_builtin/jb-ai-chat/index.js";
import { storeToRefs } from "pinia";
import { AccessToken } from "./aliyuntoken";

// 声明环境变量类型
declare interface ImportMetaEnv {
    VITE_APP_ID: string;
    VITE_ALIYUN_APPKEY: string;
    VITE_ALIYUN_ACCESS_KEY_ID: string;
    VITE_ALIYUN_ACCESS_KEY_SECRET: string;
}

// WebSocket相关
const chatInstance = ref<any>(null);
const webSocketStore = storeToRefs(useWebSocketStore());
const session = ref<any>(null);

// 监听WebSocket连接
watch(
    () => webSocketStore.ws.value,
    () => {
        console.debug("websock重连");
        if (chatInstance.value) {
            chatInstance.value.updateWebSocket(webSocketStore.ws.value);
        }
    }
);

// 创建会话
function addSession() {
    JBoltApi.post("/aiAppSession/add", {
        name: "语音转写会话",
        appId: import.meta.env.VITE_APP_ID,
    }).then((res) => {
        session.value = res.result.data;
        initWebsocket();
    });
}

// 初始化WebSocket
function initWebsocket() {
    chatInstance.value = new AiChat(null, {
        websocketInstance: webSocketStore.ws.value,
        app: import.meta.env.VITE_APP_ID,
        sessionId: session.value.id,
        global: null,
        onMsgSend: (msg) => {
            console.debug("发送消息：", msg);
        },
        onMsgReceive: (msg) => {
            switch (msg.action) {
                case "response":
                    try {
                        if (msg.data && msg.data.content && msg.data.content.text) {
                            // 尝试解析JSON
                            try {
                                const result = JSON.parse(msg.data.content.text);

                                // 将JSON转换为自然语言
                                let naturalText = "";

                                // 添加标题
                                if (result.title) {
                                    naturalText += `标题：${result.title}\n\n`;
                                }

                                // 添加原始内容
                                if (result.originalText) {
                                    naturalText += `原始内容：\n${result.originalText}\n\n`;
                                }

                                // 添加优化后的内容
                                if (result.optimizedText) {
                                    naturalText += `优化后的内容：\n${result.optimizedText}\n\n`;
                                }

                                // 添加关键点
                                if (result.keyPoints && result.keyPoints.length > 0) {
                                    naturalText += "关键信息：\n";
                                    result.keyPoints.forEach((point) => {
                                        naturalText += `- ${point.type}：${point.content}\n`;
                                    });
                                    naturalText += "\n";
                                }

                                // 添加格式信息
                                if (result.formatting) {
                                    if (result.formatting.highlights && result.formatting.highlights.length > 0) {
                                        naturalText += "重点内容：\n";
                                        result.formatting.highlights.forEach((highlight) => {
                                            naturalText += `- ${highlight}\n`;
                                        });
                                        naturalText += "\n";
                                    }

                                    if (result.formatting.suggestions && result.formatting.suggestions.length > 0) {
                                        naturalText += "优化建议：\n";
                                        result.formatting.suggestions.forEach((suggestion) => {
                                            naturalText += `- ${suggestion}\n`;
                                        });
                                        naturalText += "\n";
                                    }
                                }

                                // 添加摘要
                                if (result.summary) {
                                    naturalText += `内容摘要：\n${result.summary}\n`;
                                }

                                // 更新显示内容
                                transcriptText.value = naturalText;
                            } catch (error) {
                                // 如果不是JSON格式，直接显示文本
                                transcriptText.value = msg.data.content.text;
                            }

                            // 滚动到底部
                            if (transcriptContainer.value) {
                                transcriptContainer.value.scrollTop = transcriptContainer.value.scrollHeight;
                            }
                        }
                    } catch (error) {
                        console.error("解析消息失败:", error);
                        // 如果解析失败，直接显示原始文本
                        if (msg.data && msg.data.content && msg.data.content.text) {
                            transcriptText.value = msg.data.content.text;
                            if (transcriptContainer.value) {
                                transcriptContainer.value.scrollTop = transcriptContainer.value.scrollHeight;
                            }
                        }
                    }
                    break;
                case "over":
                    isTranscribing.value = false;
                    break;
                case "error":
                    isTranscribing.value = false;
                    showToast(msg.data.msg);
                    break;
            }
        },
    });
}

// 阿里云语音识别相关
const accessToken = ref({
    token: null as string | null,
    expireTime: null as number | null,
});
const audioContext = ref<AudioContext | null>(null);
const audioProcessor = ref<ScriptProcessorNode | null>(null);
const pcmBuffer = ref(new Int16Array());
const mediaRecorder = ref<MediaRecorder | null>(null);
const websocket = ref<WebSocket | null>(null);

// 状态变量
const isRecording = ref(false);
const recordingTime = ref(0);
const realtimeText = ref("");
const transcriptText = ref("");
const transcriptContainer = ref<HTMLElement | null>(null);
const isTranscribing = ref(false);
let timer: number | null = null;

// 格式化时间
const formatTime = computed(() => {
    const minutes = Math.floor(recordingTime.value / 60);
    const seconds = recordingTime.value % 60;
    return `${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
});

// 生成唯一ID
const generateUniqueID = () => {
    return Array(32)
        .fill(0)
        .map(() => Math.floor(Math.random() * 16).toString(16))
        .join("");
};

// 简化数据结构
interface Sentence {
  text: string;
}

// 添加新的响应式变量
const sentences = ref<Sentence[]>([]);
const currentSentence = ref<Sentence | null>(null);

// 修改处理函数
const onSentenceBegin = () => {
  currentSentence.value = {
    text: ''
  };
};

const onTextChanged = (payload: any) => {
  if (currentSentence.value) {
    currentSentence.value.text = payload.result;
    // 直接更新 realtimeText
    realtimeText.value = sentences.value
      .map(s => s.text)
      .join('\n') + '\n' + currentSentence.value.text;
  }
};

const onTextFinished = (payload: any) => {
  if (currentSentence.value) {
    currentSentence.value.text = payload.result;
    sentences.value.push({...currentSentence.value});
    currentSentence.value = null;
    // 直接更新 realtimeText
    realtimeText.value = sentences.value
      .map(s => s.text)
      .join('\n');
  }
};

// 重采样音频
const resampleAudioBuffer = async (audioBuffer: AudioBuffer, targetSampleRate: number) => {
    const numberOfChannels = audioBuffer.numberOfChannels;
    const offlineContext = new OfflineAudioContext(numberOfChannels, audioBuffer.duration * targetSampleRate, targetSampleRate);

    const bufferSource = offlineContext.createBufferSource();
    bufferSource.buffer = audioBuffer;
    bufferSource.connect(offlineContext.destination);
    bufferSource.start();

    const resampledAudioBuffer = await offlineContext.startRendering();
    return resampledAudioBuffer;
};

// 开始捕获音频
const startCaptureAudio = async () => {
    audioContext.value = new AudioContext();
    const sampleRate = audioContext.value.sampleRate;
    console.log("Original sampleRate: ", sampleRate);

    const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
    const source = audioContext.value.createMediaStreamSource(stream);

    audioProcessor.value = audioContext.value.createScriptProcessor(1024, 1, 1);
    audioProcessor.value.onaudioprocess = async (event: AudioProcessingEvent) => {
        const inputBuffer = event.inputBuffer;

        // 重采样
        let resampledBuffer = inputBuffer;
        if (sampleRate !== 16000) {
            resampledBuffer = await resampleAudioBuffer(inputBuffer, 16000);
        }

        const pcmData = convertToPCM(resampledBuffer);
        pcmBuffer.value = concatenateTypedArrays(pcmBuffer.value, new Int16Array(pcmData));

        while (pcmBuffer.value.length * 2 >= 3200) {
            const chunk = pcmBuffer.value.slice(0, 3200 / 2);
            websocket.value?.send(chunk.buffer);
            pcmBuffer.value = pcmBuffer.value.slice(3200 / 2);
        }
    };

    source.connect(audioProcessor.value);
    audioProcessor.value.connect(audioContext.value.destination);
};

// 转换PCM
const convertToPCM = (audioBuffer: AudioBuffer) => {
    let float32Array = audioBuffer.getChannelData(0);
    let pcmData = new Int16Array(float32Array.length);

    for (let i = 0; i < float32Array.length; i++) {
        let s = Math.max(-1, Math.min(1, float32Array[i]));
        pcmData[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
    }

    return pcmData.buffer;
};

// 连接TypedArray
const concatenateTypedArrays = (a: Int16Array, b: Int16Array) => {
    const result = new Int16Array(a.length + b.length);
    result.set(a);
    result.set(b, a.length);
    return result;
};

// 开始录音
const startRecording = async () => {
    isRecording.value = true;
    recordingTime.value = 0;

    // 开始计时
    timer = window.setInterval(() => {
        if (isRecording.value) {
            recordingTime.value++;
        }
    }, 1000);

    // 连接阿里云
    const APPKEY = import.meta.env.VITE_ALIYUN_APPKEY;
    const TOKEN = import.meta.env.VITE_ALIYUN_TOKEN;
    if (!TOKEN) {
        showToast("Token未初始化，请稍后重试");
        return;
    }
    const URL = `wss://nls-gateway-cn-beijing.aliyuncs.com/ws/v1?token=${TOKEN}`;

    websocket.value = new WebSocket(URL);
    websocket.value.onopen = () => {
        const instruction = {
            header: {
                appkey: APPKEY,
                message_id: generateUniqueID(),
                task_id: generateUniqueID(),
                namespace: "SpeechTranscriber",
                name: "StartTranscription",
            },
            payload: {
                format: "PCM",
                sample_rate: 16000,
                enable_intermediate_result: true,
                enable_punctuation_prediction: true,
                enable_inverse_text_normalization: true,
                max_sentence_silence: 500,
            },
        };
        websocket.value?.send(JSON.stringify(instruction));
    };

    websocket.value.onmessage = (e) => {
        const ret = JSON.parse(e.data);
        console.log('收到消息类型:', ret.header.name);
        console.log('完整消息:', ret);
        
        if (ret.header.name === "TranscriptionResultChanged") {
            if (ret.payload && ret.payload.result) {
                onTextChanged(ret.payload);
            }
        } else if (ret.header.name === "SentenceBegin") {
            onSentenceBegin();
        } else if (ret.header.name === "TranscriptionStarted") {
            startCaptureAudio();
        } else if (ret.header.name === "SentenceEnd") {
            if (ret.payload && ret.payload.result) {
                onTextFinished(ret.payload);
            }
        } else if (ret.header.name === "TranscriptionCompleted") {
            console.log("服务端已停止了语音转写", ret);
        } else if (ret.header.name === "TaskFailed") {
            console.error("转写任务失败:", ret);
            showToast("转写任务失败，请重试");
            stopRecording();
        }
    };
};

// 停止录音
const stopRecording = () => {
    isRecording.value = false;

    // 清除计时器
    if (timer) {
        clearInterval(timer);
        timer = null;
    }

    if (audioProcessor.value) {
        audioProcessor.value.disconnect();
        audioProcessor.value = null;
    }
    if (audioContext.value) {
        audioContext.value.close();
        audioContext.value = null;
    }
    if (websocket.value) {
        websocket.value.close();
        websocket.value = null;
    }

    // 关闭麦克风
    navigator.mediaDevices
        .getUserMedia({ audio: true })
        .then((stream) => {
            stream.getTracks().forEach((track) => track.stop());
        })
        .catch((err) => console.error("关闭麦克风失败:", err));
};

// 切换录音状态
const toggleRecording = () => {
    if (isRecording.value) {
        stopRecording();
    } else {
        startRecording();
    }
};

// 重置录音
const resetRecording = () => {
    stopRecording();
    recordingTime.value = 0;
    realtimeText.value = "";
    transcriptText.value = "";
    sentences.value = [];
    currentSentence.value = null;
};

// 完成转写
const completeTranscription = () => {
    if (recordingTime.value === 0) {
        showToast("暂无可转写内容，请先进行录音");
        return;
    }

    stopRecording();
    isTranscribing.value = true;
    transcriptText.value = ""; // 清空之前的内容

    // 发送实时转写内容到后端
    chatInstance.value?.sendMessage({
        action: "request",
        data: {
            role: "user",
            content: {
                text: realtimeText.value,
                type: "voice",
            },
        },
    });
};

// 复制文本
const copyRealTimeText = () => {
    if (realtimeText.value) {
        navigator.clipboard.writeText(realtimeText.value);
        showToast("实时转写内容已复制");
    }
};

const copyText = () => {
    if (transcriptText.value) {
        navigator.clipboard.writeText(transcriptText.value);
        showToast("AI 转写内容已复制");
    }
};

// 导出文本
const exportText = () => {
    if (transcriptText.value) {
        const blob = new Blob([transcriptText.value], { type: "text/plain" });
        const url = URL.createObjectURL(blob);
        const a = document.createElement("a");
        a.href = url;
        a.download = "转写文本.txt";
        a.click();
        URL.revokeObjectURL(url);
    }
};

// 显示提示消息
const showToast = (message: string) => {
    const toastContainer = document.createElement("div");
    document.body.appendChild(toastContainer);
    const toastApp = createApp({
        setup() {
            return () =>
                h(
                    "div",
                    {
                        class: "fixed top-4 left-1/2 transform -translate-x-1/2 bg-gray-800 text-white px-6 py-3 rounded-full shadow-lg z-50 animate-fade-in-down",
                        style: "animation: fadeInDown 0.3s ease-out, fadeOut 0.3s ease-out 2.7s forwards;",
                    },
                    message
                );
        },
    });
    toastApp.mount(toastContainer);
    setTimeout(() => {
        toastApp.unmount();
        document.body.removeChild(toastContainer);
    }, 3000);
};

// 组件挂载时初始化
onMounted(async () => {
    addSession();

    // 初始化阿里云Token
    const appKey = import.meta.env.VITE_ALIYUN_APPKEY;
    const token = import.meta.env.VITE_ALIYUN_TOKEN;
    accessToken.value = { token, expireTime: Date.now() + 24 * 60 * 60 * 1000 }; // 设置24小时过期
});

// 在 script setup 部分添加以下代码
const realtimeContainer = ref<HTMLElement | null>(null);
</script>
<style scoped>
.custom-scrollbar {
    overflow-y: auto;
}

.custom-scrollbar::-webkit-scrollbar {
    width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
    background: rgba(243, 244, 246, 0.5);
    border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
    background: rgba(99, 102, 241, 0.3);
    border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
    background: rgba(99, 102, 241, 0.5);
}

/* 隐藏 textarea 的滚动条 */
textarea {
    scrollbar-width: none;  /* Firefox */
    -ms-overflow-style: none;  /* IE and Edge */
}

textarea::-webkit-scrollbar {
    display: none;  /* Chrome, Safari, Opera */
}
.pulse-animation {
    animation: pulse 2s infinite;
}
@keyframes pulse {
    0% {
        box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.4);
    }
    70% {
        box-shadow: 0 0 0 20px rgba(99, 102, 241, 0);
    }
    100% {
        box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
    }
}
.typing-indicator {
    display: flex;
    gap: 4px;
    padding: 8px;
}
.typing-indicator span {
    width: 8px;
    height: 8px;
    background-color: #6366f1;
    border-radius: 50%;
    animation: bounce 1.4s infinite ease-in-out;
}
.typing-indicator span:nth-child(1) {
    animation-delay: -0.32s;
}
.typing-indicator span:nth-child(2) {
    animation-delay: -0.16s;
}
@keyframes bounce {
    0%,
    80%,
    100% {
        transform: scale(0);
    }
    40% {
        transform: scale(1);
    }
}
@keyframes fadeInDown {
    from {
        opacity: 0;
        transform: translate(-50%, -20px);
    }
    to {
        opacity: 1;
        transform: translate(-50%, 0);
    }
}
@keyframes fadeOut {
    from {
        opacity: 1;
    }
    to {
        opacity: 0;
    }
}
.sentence {
    padding: 8px 12px;
    border-radius: 8px;
    transition: all 0.3s ease;
}

.completed {
    background-color: #f3f4f6;
    color: #374151;
    border-left: 3px solid #9ca3af;
}

.processing {
    background-color: #eef2ff;
    color: #4f46e5;
    border-left: 3px solid #4f46e5;
    animation: pulse 2s infinite;
}

@keyframes pulse {
    0% {
        background-color: #eef2ff;
    }
    50% {
        background-color: #e0e7ff;
    }
    100% {
        background-color: #eef2ff;
    }
}
</style>
